<?php
/**
 * 子进程
 * User: dxk
 * Date: 2020/07/17
 */
namespace App\Library\Service;

use Illuminate\Support\ProcessUtils;
use Symfony\Component\Process\PhpExecutableFinder;

/**
 * 创建多进程
 * Class PcntlFork
 * @package Easy\Library\Service
 */
class PcntlFork
{
    //创建的进程
    private $threads = [];

    public function __construct()
    {
        if (!function_exists("pcntl_fork")) {
            throw new \Exception('当前环境不支持多进程', 500);
        }
    }

    /**
     * 子进程执行代码
     * @param $command
     * @param array $parameters
     * @return int
     * @throws \Exception
     */
    public function exec($command, $parameters = [])
    {
        $pid = pcntl_fork();
        if ($pid == -1) {
            throw new \Exception("fork进程失败");
        }
        //如果子进程启动则返回pid
        if ($pid) {
            $this->threads[$pid] = $pid;
            return $pid;
        }
        //如果当前是子进程则执行待执行的程序
        $binary = ProcessUtils::escapeArgument((new PhpExecutableFinder())->find(false));
        if (defined('HHVM_VERSION')) {
            $binary .= ' --php';
        }
        $binary = trim($binary, "'");
        pcntl_exec($binary, self::compileParameters($command, $parameters), $_SERVER);
        exit(0);
    }

    /**
     * 拼接参数
     * @param $command
     * @param array $parameters
     * @return array
     */
    private function compileParameters($command, $parameters = [])
    {
        $argv = collect($parameters)->map(function ($value, $key) {
            return is_numeric($key) ? $value : $key . '=' . (is_numeric($value) ? $value : ProcessUtils::escapeArgument($value));
        })->all();
        $argv = array_values($argv);
        array_unshift($argv, $command);
        $artisan = base_path() . DIRECTORY_SEPARATOR . 'artisan';
        array_unshift($argv, $artisan);
        return $argv;
    }

    /**
     * 等待进程
     * @param int $pid 待等待的进程，如果为null则等待所有进程
     * @return $this
     * @throws \Exception
     */
    public function wait($pid = null)
    {
        if ($pid) {
            $threads = [$pid];
        } else {
            $threads = $this->threads;
        }
        foreach ($threads as $pid) {
            if (!self::checkPidAlive($pid)) {
                unset($this->threads[$pid]);
            }
        }
        return $this;
    }

    /**
     * 检测进程是否存在
     * @param $pid
     * @return bool
     */
    private function checkPidAlive($pid)
    {
        return pcntl_waitpid($pid, $status, WNOHANG) === 0;
    }


    /**
     * 获取PID集合.
     * @return array
     */
    public function getPIDs()
    {
        return array_values($this->threads);
    }

    /**
     * 获取当前进程数
     * @return int
     */
    public function getAlivePidNum()
    {
        return count($this->wait()->getPIDs());
    }

    /**
     * If no call to wait() is made, then we wait for the threads on destruct.
     */
    public function __destruct()
    {
        $this->wait();
    }
}
